<?php
/**
 * LilDate Engine
 * 
 * @copyright     Copyright 2011, Miha Nahtigal (http://www.nahtigal.com)
 * @license       http://www.opensource.org/licenses/mit-license.php The MIT License
 * 
 * 
 * This LilDate behavior for CakePHP allows you to auto convert dates from local
 * format to default format suitable for saving into database
 * 
 */
class LilDateEngine {
/**
 * An array of settings
 */
    var $_settings = array();

/**
 * Instantiates object and its settings
 * 
 * @param array $settings Array of settings
 * @access public
 * @return void
 */
	public function __construct($settings = array()) {
		$settings = (array)$settings;
		
		$_defaults = array(
			'dateSeparator' => ($s = Configure::read('dateSeparator')) ? $s : '-',
			'dateFormat' => ($f = Configure::read('dateFormat')) ? $f : 'YMD',
			'timeFormat' => ($f = Configure::read('timeFormat')) ? $f : '12'
		);
		
		$this->_settings = array_merge($_defaults, $settings);
	}
/**
 * Instantiate static instance
 * 
 * @param array $settings Array of scope properties
 * @access public
 * @return object
 */
	public static function getInstance($settings = array()) {
		static $instance = array();
		if (!$instance) {
			$instance = new LilDateEngine($settings);
		}
		return $instance;
	}
/**
 * Prevent object from clonning itself
 * 
 * @access protected
 * @return void
 */
	protected function __clone() { }
/**
 * Sets new date separator
 *
 * @param string $dateSeparator New date separator
 * @access public
 * @return void
 */
	function setDateSeparator($dateSeparator = null) {
		if (!empty($dateSeparator)) {
			$this->_settings['dateSeparator'] = $dateSeparator;
		}
	}
/**
 * Sets new time format
 *
 * @param string $timeFormat New time format
 * @access public
 * @return void
 */
	function setTimeFormat($timeFormat = null) {
		if (!empty($timeFormat)) {
			$this->_settings['timeFormat'] = $timeFormat;
		}
	}
/**
 * Sets new date format
 *
 * @param string $dateFormat New date format (YMD, DMY,..)
 * @access public
 * @return void
 */
	function setDateFormat($dateFormat = null) {
		$this->_settings['dateFormat'] = $dateFormat;
	}
/**
 * decimalSeparator method
 *
 * @access public
 * @return string
 */
	function dateSeparator() {
		return $this->_settings['dateSeparator'];
	}
/**
 * dateFormat method
 *
 * @access public
 * @return string
 */
	function dateFormat() {
		return $this->_settings['dateFormat'];
	}
/**
 * timeFormat method
 *
 * @access public
 * @return string
 */
	function timeFormat() {
		return $this->_settings['timeFormat'];
	}
/**
 * Converts local date to general representation eg. YYYY-MM-DD
 * 
 * @param string $localDate Date in local format
 * @param array $options Options which can override default settings
 * @access public
 * @return string
 */
	function delocalize($localDate, $options = array()) {
		if ($this->isSql($localDate)) return $localDate;
		$settings = array_merge($this->_settings, (array)$options);
		$parts = explode($settings['dateSeparator'], $localDate);
		$order = array_flip(str_split($settings['dateFormat']));
		$ret = '';
		return implode('-', array($parts[$order['Y']], $parts[$order['M']], $parts[$order['D']]));
	}
/**
 * Returns regex for date check
 *
 * @param array $options Options which can override default settings, 
 * @return string Regex
 * @access public
 */
	function getCheckDateRegex($options = array()) {
		$settings = array_merge($this->_settings, (array)$options);
		
		$parts = array(
			'Y' => '([1-9][0-9]{0,3})',
			'M' => '((0[1-9])|(1[0-2]))',
			'D' => '((0[1-9])|([1-2][0-9])|(3[0-1]))'
		);
		$order = str_split($settings['dateFormat']);
		
		$regex = '^' . implode(
			'\\' . $settings['dateSeparator'],
			array($parts[$order[0]], $parts[$order[1]], $parts[$order[2]])
		) . '$';
		
		return $regex;
	}
/**
 * age method
 *
 * Returns age from a date
 *
 * @param mixed $dateString
 * @param mixed $userOffset
 * @access public
 * @return int
 */
	function age($dateString=null, $userOffset = null) {
		if (strlen($dateString)==4) $dateString .= '-12-31';
		$date = $dateString ? $this->fromString($dateString, $userOffset) : time();

		$ret = date("Y")-date("Y", $date);
		if (date("m", $date) > date("m")) $ret--;
		else if (date("m", $date)==date("m") && date("d", $date) > date("d")) $ret--;
		return $ret;
	}
/**
 * format method
 *
 * Returns common date representation
 *
 * @param mixed $dateString
 * @param mixed $userOffset
 * @access public
 * @return string
 */
	function format($dateString = null, $format = null, $userOffset = null) {
		$date = $dateString ? $this->fromString($dateString, $userOffset) : time();
		
		if (is_null($format)) {
			$parts = array('Y' => '%Y', 'M' => '%m', 'D' => '%d');
			
			$order = str_split($this->dateFormat());
			$format = array($parts[$order[0]], $parts[$order[1]], $parts[$order[2]]);
			$format = implode($this->dateSeparator(), $format);
		}
		
		$format = $this->convertSpecifiers($format, $date);
		return strftime($format, $date);
	}
/**
 * dateTimeFormat method
 *
 * Returns nice date representation
 *
 * @param mixed $seconds
 * @access public
 * @return string
 */
	function dateTimeFormat($dateString = null, $userOffset = null) {
		$date = $dateString ? $this->fromString($dateString, $userOffset) : time();
		
		if (!$format = Configure::read('dateTimeFormat')) {
			$format = '%c';
		}
		$format = $this->convertSpecifiers($format, $date);
		return strftime($format, $date);
	}
/**
 * niceShortDate method
 *
 * Returns nice date representation
 *
 * @param mixed $seconds
 * @access public
 * @return string
 */
	function niceShortDate($dateString = null, $userOffset = null, $useWords = true) {
		$date = $dateString ? $this->fromString($dateString, $userOffset) : time();
		$y = $this->isThisYear($date) ? '' : ' %Y';
		if ($useWords && $this->isToday($date)) {
			$ret = __d('lil', 'Today');
		} elseif ($useWords && $this->wasYesterday($date)) {
			$ret = __d('lil', 'Yesterday');
		} elseif ($useWords && $this->isTomorrow($date)) {
			$ret = __d('lil', 'Tomorrow');
		} else {
			if (!$nice_short_format = Configure::read('shortDateFormat')) {
				$nice_short_format = '%b %eS';
			}
			
			$format = $this->convertSpecifiers(trim($nice_short_format . ' ' . $y), $date);
			$ret = strftime($format, $date);
		}
		return $ret;
	}
/**
 * toHoursAndMinutes method
 *
 * Returns hourly representation of seconds
 *
 * @param mixed $seconds
 * @access public
 * @return string
 */
	function toHoursAndMinutes($seconds = 0, $show_hours = true, $separator = ', ') {
		$zeroes = false;
		$pad_mins = null;
		
		if (is_array($show_hours)) {
			if (isset($show_hours['separator'])) $separator = $show_hours['separator'];
			if (isset($show_hours['zeroes'])) $zeroes = $show_hours['zeroes'];
			
			
			if (isset($show_hours['pad_mins'])) $pad_mins = $show_hours['pad_mins'];
			if ($pad_mins === true) $pad_mins = '0';
			
			// must be last
			if (isset($show_hours['hours'])) $show_hours = $show_hours['hours']; else $show_hours = true;
		}
		
		$negative = $seconds < 0;
		$seconds = abs($seconds);
		$ret = '';
		$hours = 0;
		$minutes = 0;
		
		$hours = (int)floor($seconds / 3600);
		$minutes = (int)round(($seconds - ($hours * 3600)) / 60);
		if ((($hours > 0) && $show_hours) || $zeroes === 'both') {
			if ($minutes > 0 || $zeroes === 'both') {
				$mins_display = $minutes;
				if (isset($pad_mins)) $mins_display = str_pad($minutes, 2, $pad_mins, STR_PAD_LEFT);
				
				$ret .= 
					$hours . ' ' .
					__dn('lil', 'hr', 'hrs', $hours) .
					$separator .
					$mins_display . ' ' .
					__dn('lil', 'min', 'mins', $minutes);
			} else {
				$ret .= $hours . ' ' . __dn('lil', 'hr', 'hrs', $hours);
			}
		} else if ($minutes > 0) {
			$minutes = $mins_display = $minutes + $hours * 60;
			if (isset($pad_mins)) $mins_display = str_pad($minutes, 2, $pad_mins, STR_PAD_LEFT);
			
			$ret .= $mins_display . ' ' . __dn('lil', 'min', 'mins', $minutes);
		}
		
		if ($hours == 0 && $minutes == 0 && $zeroes !== false && $zeroes !== 'both') {
			if ($zeroes === 'minutes') {
				$ret .= '0 ' . __dn('lil', 'min', 'mins', 0); 
			} else {
				$ret .= '0 ' . __dn('lil', 'hr', 'hrs', 0);
			}
		}
		
		if ($negative) $ret = '- ' . $ret;
		return $ret;
	}
/**
 * Checks that a value is a valid date.
 *
 * @param string $localDate Date in local foramt
 * @return boolean Success
 * @access public
 */
	function isValidDate($localDate, $options = array()) {
		return (bool)preg_match("/" . $this->getCheckDateRegex($options) . "/", $localDate);
	}
/**
 * isFuture
 *
 * @access public
 */
	function isFuture($dateString, $userOffset = null, $fromDateString = null) {
		$date = $this->fromString($dateString, $userOffset);
		if (!empty($fromDateString)) {
			$from = $this->fromString($fromDateString, $userOffset);
			return date('Y-m-d', $date) > date('Y-m-d', $from);
		} else {
			return date('Y-m-d', $date) > date('Y-m-d', time());
		}
	}
/**
 * isPast
 *
 * @access public
 */
	function isPast($dateString, $userOffset = null, $fromDateString = null) {
		$date = $this->fromString($dateString, $userOffset);
		if (!empty($fromDateString)) {
			$from = $this->fromString($fromDateString, $userOffset);
			return date('Y-m-d', $date) < date('Y-m-d', $from);
		} else {
			return date('Y-m-d', $date) < date('Y-m-d', time());
		}
	}
/**
 * Returns true if given datetime string is today.
 *
 * @param string $dateString Datetime string or Unix timestamp
 * @param int $userOffset User's offset from GMT (in hours)
 * @return boolean True if datetime string is today
 */
	public function isToday($dateString, $userOffset = null) {
		$date = $this->fromString($dateString, $userOffset);
		return date('Y-m-d', $date) == date('Y-m-d', time());
	}

/**
 * Returns true if given datetime string is within this week.
 *
 * @param string $dateString
 * @param int $userOffset User's offset from GMT (in hours)
 * @return boolean True if datetime string is within current week
 * @access public
 * @link http://book.cakephp.org/view/1472/Testing-Time
 */
	public function isThisWeek($dateString, $userOffset = null) {
		$date = $this->fromString($dateString, $userOffset);
		return date('W o', $date) == date('W o', time());
	}

/**
 * Returns true if given datetime string is within this month
 * @param string $dateString
 * @param int $userOffset User's offset from GMT (in hours)
 * @return boolean True if datetime string is within current month
 * @access public
 * @link http://book.cakephp.org/view/1472/Testing-Time
 */
	public function isThisMonth($dateString, $userOffset = null) {
		$date = $this->fromString($dateString);
		return date('m Y',$date) == date('m Y', time());
	}

/**
 * Returns true if given datetime string is within current year.
 *
 * @param string $dateString Datetime string or Unix timestamp
 * @return boolean True if datetime string is within current year
 * @access public
 * @link http://book.cakephp.org/view/1472/Testing-Time
 */
	public function isThisYear($dateString, $userOffset = null) {
		$date = $this->fromString($dateString, $userOffset);
		return  date('Y', $date) == date('Y', time());
	}

/**
 * Returns true if given datetime string was yesterday.
 *
 * @param string $dateString Datetime string or Unix timestamp
 * @param int $userOffset User's offset from GMT (in hours)
 * @return boolean True if datetime string was yesterday
 * @access public
 * @link http://book.cakephp.org/view/1472/Testing-Time
 *
 */
	public function wasYesterday($dateString, $userOffset = null) {
		$date = $this->fromString($dateString, $userOffset);
		return date('Y-m-d', $date) == date('Y-m-d', strtotime('yesterday'));
	}

/**
 * Returns true if given datetime string is tomorrow.
 *
 * @param string $dateString Datetime string or Unix timestamp
 * @param int $userOffset User's offset from GMT (in hours)
 * @return boolean True if datetime string was yesterday
 * @access public
 * @link http://book.cakephp.org/view/1472/Testing-Time
 */
	public function isTomorrow($dateString, $userOffset = null) {
		$date = $this->fromString($dateString, $userOffset);
		return date('Y-m-d', $date) == date('Y-m-d', strtotime('tomorrow'));
	}
	
/**
 * Returns true if two given datetimes are the same day
 *
 * @param string $dateString1 First date/time.
 * @param string $dateString2 Second date/time.
 * @return boolean True if given dates are within the same day
 * @access public
 */
	public function isSameDay($dateString1, $dateString2) {
		$date1 = $this->fromString($dateString1);
		$date2 = $this->fromString($dateString2);
		return date('Y-m-d', $date1) == date('Y-m-d', $date2);
	}
/**
 * Returns easter date for given year
 *
 * @param int $year
 * @return date
 * @access public
 */
	function easterDate($year) { 
		/** 
		* G is the Golden Number-1 
		* H is 23-Epact (modulo 30) 
		* I is the number of days from 21 March to the Paschal full moon 
		* J is the weekday for the Paschal full moon (0=Sunday, 1=Monday, etc.) 
		* L is the number of days from 21 March to the Sunday on or before the Paschal full moon (a number between -6 and 28) 
		**/ 
		
		$G = $year % 19; 
		$C = intval($year / 100); 
		$H = intval($C - intval($C / 4) - intval((8*$C+13) / 25) + 19*$G + 15) % 30; 
		$I = intval($H - intval($H / 28) * (1 - intval($H / 28) * intval(29 / ($H + 1)) * (intval(21 - $G) / 11))); 
		$J = ($year + intval($year/4) + $I + 2 - $C + intval($C/4)) % 7; 
		$L = $I - $J; 
		
		$m = 3 + intval(($L + 40) / 44); 
		$d = $L + 28 - 31 * (intval($m / 4)); 
		$y = $year; 
		
		return mktime(0,0,0, $m, $d, $y); 
	} 

/**
 * Converts a string representing the format for the function strftime and returns a
 * windows safe and i18n aware format.
 *
 * @param string $format Format with specifiers for strftime function.
 *    Accepts the special specifier %S which mimics th modifier S for date()
 * @param string UNIX timestamp
 * @return string windows safe and date() function compatible format for strftime
 */
	public function convertSpecifiers($format, $time = null) {
		if (!$time) {
			$time = time();
		}
		$this->__time = $time;
		return preg_replace_callback('/\%(\w+)/', array($this, '__translateSpecifier'), $format);
	}

/**
 * Auxiliary function to translate a matched specifier element from a regular expresion into
 * a windows safe and i18n aware specifier
 *
 * @param array $specifier match from regular expression
 * @return string converted element
 * @access private
 */
	private function __translateSpecifier($specifier) {
		switch ($specifier[1]) {
			case 'a':
				$abday = __dc('lil', 'abday', 5);
				if (is_array($abday)) {
					return $abday[date('w', $this->__time)];
				}
				break;
			case 'A':
				$day = __dc('lil', 'day', 5);
				if (is_array($day)) {
					return $day[date('w', $this->__time)];
				}
				break;
			case 'c':
				$format = __dc('lil', 'd_t_fmt', 5);
				if ($format != 'd_t_fmt') {
					return $this->convertSpecifiers($format, $this->__time);
				}
				break;
			case 'C':
				return sprintf("%02d", date('Y', $this->__time) / 100);
			case 'D':
				return '%m/%d/%y';
			case 'e':
				if (DS === '/') {
					return '%e';
				}
				$day = date('j', $this->__time);
				if ($day < 10) {
					$day = ' ' . $day;
				}
				return $day;
			case 'eS' :
				return date('jS', $this->__time);
			case 'b':
			case 'h':
				$months = __dc('lil', 'abmon', 5);
				if (is_array($months)) {
					return $months[date('n', $this->__time) -1];
				}
				return '%b';
			case 'B':
				$months = __dc('lil', 'mon', 5);
				if (is_array($months)) {
					return $months[date('n', $this->__time) -1];
				}
				break;
			case 'n':
				return "\n";
			case 'p':
			case 'P':
				$default = array('am' => 0, 'pm' => 1);
				$meridiem = $default[date('a',$this->__time)];
				$format = __dc('lil', 'am_pm', 5);
				if (is_array($format)) {
					$meridiem = $format[$meridiem];
					return ($specifier[1] == 'P') ? strtolower($meridiem) : strtoupper($meridiem);
				}
				break;
			case 'r':
				$complete = __dc('lil', 't_fmt_ampm', 5);
				if ($complete != 't_fmt_ampm') {
					return str_replace('%p',$this->__translateSpecifier(array('%p', 'p')),$complete);
				}
				break;
			case 'R':
				return date('H:i', $this->__time);
			case 't':
				return "\t";
			case 'T':
				return '%H:%M:%S';
			case 'u':
				return ($weekDay = date('w', $this->__time)) ? $weekDay : 7;
			case 'x':
				$format = __dc('lil', 'd_fmt', 5);
				if ($format != 'd_fmt') {
					return $this->convertSpecifiers($format, $this->__time);
				}
				break;
			case 'X':
				$format = __dc('lil', 't_fmt', 5);
				if ($format != 't_fmt') {
					return $this->convertSpecifiers($format, $this->__time);
				}
				break;
		}
		return $specifier[0];
	}

/**
 * Converts given time (in server's time zone) to user's local time, given his/her offset from GMT.
 *
 * @param string $serverTime UNIX timestamp
 * @param int $userOffset User's offset from GMT (in hours)
 * @return string UNIX timestamp
 */
	public function convert($serverTime, $userOffset) {
		$serverOffset = $this->serverOffset();
		$gmtTime = $serverTime - $serverOffset;
		$userTime = $gmtTime + $userOffset * (60*60);
		return $userTime;
	}

/**
 * Returns server's offset from GMT in seconds.
 *
 * @return int Offset
 */
	public function serverOffset() {
		return date('Z', time());
	}

/**
 * Returns a UNIX timestamp, given either a UNIX timestamp or a valid strtotime() date string.
 *
 * @param string $dateString Datetime string
 * @param int $userOffset User's offset from GMT (in hours)
 * @return string Parsed timestamp
 * @access public
 * @link http://book.cakephp.org/view/1471/Formatting
 */
	public function fromString($dateString, $userOffset = null) {
		if (empty($dateString)) {
			return false;
		}
		if (is_integer($dateString) || is_numeric($dateString)) {
			$date = intval($dateString);
		} else {
			$date = strtotime($dateString);
		}
		if ($userOffset !== null) {
			return $this->convert($date, $userOffset);
		}
		if ($date === -1) {
			return false;
		}
		return $date;
	}
/**
 * Returns a formatted date suitable for sql.
 *
 * @param string $dateString Datetime string
 * @param bool $withTime Include time 
 * @param int $userOffset User's offset from GMT (in hours)
 * @return string SQL date or datetime format
 * @access public
 */
	public function toSql($dateString, $withTime = true, $userOffset = null) {
		$date = $this->fromString($dateString, $userOffset);
		$format = '%Y-%m-%d';
		if ($withTime) $format .= ' ' . '%H:%M:%S';
		return strftime($format, $date);
	}
/**
 * Returns true if given date is in sql date format
 *
 * @param string $dateString Datetime string
 * @return bool Valid sql date format.
 * @access public
 */
	public function isSql($dateString) {
		return (bool)preg_match("/" . 
			$this->getCheckDateRegex(array('dateFormat' => 'YMD', 'dateSeparator' => '-')) . 
			"/", substr($dateString, 0, 10));
	}
}